/*
 *   Copyright 2012-present OSBI Ltd
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */

/* global pvc, pv */

// Packages
import $ from 'jquery';
import _ from 'lodash';

// Utils
import { Settings } from '../utils';
import { ROW_HEADER_HEADER, COLUMN_HEADER } from '../utils/constants';
import { log } from '../utils/dev-logs';

const CCC_OPTIONS_DEFAULT = {
  Base: {
    animate: false,
    selectable: false,
    valuesVisible: false,
    legend: true,
    legendPosition: 'top',
    legendAlign: 'right',
    compatVersion: 2,
    legendSizeMax: '30%',
    axisSizeMax: '40%',
    plotFrameVisible: false,
    orthoAxisMinorTicks: false,
    colors: Settings.CHART_COLORS
  },
  HeatGridChart: {
    orientation: 'horizontal',
    useShapes: true,
    shape: 'circle',
    nullShape: 'cross',
    colorNormByCategory: false,
    sizeRole: 'value',
    legendPosition: 'right',
    legend: true,
    hoverable: true,
    axisComposite: true,
    colors: ['red', 'yellow', 'lightgreen', 'darkgreen'],
    yAxisSize: '20%'
  },
  WaterfallChart: {
    orientation: 'horizontal'
  },
  PieChart: {
    multiChartColumnsMax: 3,
    multiChartMax: 30,
    smallTitleFont: 'bold 14px sans-serif',
    valuesVisible: true,
    valuesMask: '{category} / {value.percent}',
    explodedSliceRadius: '10%',
    extensionPoints: {
      slice_innerRadiusEx: '40%',
      slice_offsetRadius(scene) {
        return scene.isSelected() ? '10%' : 0;
      }
    },
    clickable: true
    //valuesLabelStyle: 'inside'
  },
  LineChart: {
    extensionPoints: {
      area_interpolate: 'monotone', // cardinal
      line_interpolate: 'monotone'
    }
  },
  StackedAreaChart: {
    extensionPoints: {
      area_interpolate: 'monotone',
      line_interpolate: 'monotone'
    }
  },
  TreemapChart: {
    legendPosition: 'right',
    multiChartIndexes: 0,
    extensionPoints: {
      leaf_lineWidth: 2
    },
    layoutMode: 'slice-and-dice',
    valuesVisible: true
  },
  SunburstChart: {
    valuesVisible: false,
    hoverable: false,
    selectable: false,
    clickable: false,
    multiChartIndexes: [0],
    multiChartMax: 30
  }
};

class SaikuChartRenderer {
  constructor(data, options) {
    this.rawdata = data;
    this.cccOptions = {};
    this.data = null;
    this.hasProcessed = false;
    this.hasRendered = false;

    if (!options && !options.hasOwnProperty('htmlObject')) {
      log(
        'You need to supply a html object in the options for the SaikuChartRenderer!',
        'warn'
      );
    }

    this.el = $(options.htmlObject);
    this.id = _.uniqueId('chart_');

    $(this.el).html(
      `<div class="sku-canvas-wrapper" style="display: none;"><div id="canvas_${this.id}"></div></div>`
    );

    this.zoom = options.zoom;

    if (options.zoom) {
      const self = this;
      const btns = `
        <span class="sku-zoom-button-group bp3-button-group bp3-minimal bp3-vertical" style="float: left;">
          <a href="#" class="bp3-button bp3-icon-refresh sku-button-rerender" title="Re-render chart"></a>
          <a href="#" class="bp3-button bp3-icon-arrow-up sku-button-zoomout" style="display: none;" title="Zoom back out"></a>
        </span>
      `;

      $(btns).prependTo($(this.el).find('.sku-canvas-wrapper'));

      $(this.el)
        .find('.sku-button-zoomout')
        .on('click', event => {
          event.preventDefault();
          self.zoomOut();
        });

      $(this.el)
        .find('.zoomin')
        .on('click', event => {
          event.preventDefault();
          self.zoomIn();
        });

      $(this.el)
        .find('.sku-button-rerender')
        .on('click', event => {
          event.preventDefault();
          $(self.el)
            .find('.sku-button-zoomout')
            .hide();
          self.switchChart(self.type);
        });
    }

    if (options.chartDefinition) {
      this.chartDefinition = options.chartDefinition;
    }

    this.cccOptions.canvas = `canvas_${this.id}`;
    this.data = null;
    this.adjustSizeTo = null;

    if (options.adjustSizeTo) {
      this.adjustSizeTo = options.adjustSizeTo;
    } else {
      this.adjustSizeTo = options.htmlObject;
    }

    if (this.rawdata) {
      if (this.type === 'sunburst') {
        this.processDataTree({ data: this.rawdata });
      } else {
        this.processDataTree({ data: this.rawdata }, true, true);
      }
    }

    if (options.mode) {
      this.switchChart(options.mode);
    } else {
      // Default
      this.switchChart('stackedBar');
    }

    this.adjust();
  }

  adjust() {
    const self = this;
    const calculateLayout = () => {
      if (self.hasRendered && $(self.el).is(':visible')) {
        self.switchChart(self.type);
      }
    };
    const lazyLayout = _.debounce(calculateLayout, 300);

    $(window).resize(() => {
      $(self.el)
        .find('.sku-canvas-wrapper')
        .fadeOut(150);
      lazyLayout();
    });
  }

  zoomIn() {
    const chart = this.chart.root;
    const data = chart.data;

    $(this.el)
      .find('.sku-canvas-wrapper')
      .hide();

    data.datums(null, { selected: false }).each(datum => {
      datum.setVisible(false);
    });

    data.clearSelected();

    chart.render(true, true, false);

    this.renderChartElement();
  }

  zoomOut() {
    const chart = this.chart.root;
    const data = chart.data;
    const kData = chart.keptVisibleDatumSet;

    if (kData === null || kData.length === 0) {
      $(this.el)
        .find('.sku-button-zoomout')
        .hide();
    } else if (kData.length === 1) {
      $(this.el)
        .find('.sku-button-zoomout')
        .hide();
      chart.keptVisibleDatumSet = [];
      pvc.data.Data.setVisible(data.datums(null, { visible: false }), true);
    } else if (kData.length > 1) {
      const nonVisible = data.datums(null, { visible: false }).array();
      const back = chart.keptVisibleDatumSet[kData.length - 1];

      chart.keptVisibleDatumSet.splice(kData.length - 1, 1);

      _.intersection(back, nonVisible).forEach(datum => {
        datum.setVisible(true);
      });
    }

    chart.render(true, true, false);
  }

  render() {
    _.delay(this.renderChartElement, 0, this);
  }

  switchChart(key, override) {
    if (
      override &&
      (override !== null || override !== undefined) &&
      (override.chartDefinition !== null ||
        override.chartDefinition !== undefined)
    ) {
      this.chartDefinition = override.chartDefinition;

      if (
        override &&
        override.chartDefinition &&
        override.chartDefinition.type !== undefined
      ) {
        this.type = override.chartDefinition.type;
      }

      if (override && override.hasProcessed !== undefined) {
        this.hasProcessed = override.hasProcessed;
      }
    }

    const keyOptions = {
      stackedBar: {
        type: 'BarChart',
        stacked: true
      },
      bar: {
        type: 'BarChart'
      },
      multiplebar: {
        type: 'BarChart',
        multiChartIndexes: [1],
        dataMeasuresInColumns: true,
        orientation: 'vertical',
        smallTitlePosition: 'top',
        multiChartMax: 30,
        multiChartColumnsMax: Math.floor(this.cccOptions.width / 200),
        smallWidth: 200,
        smallHeight: 150
      },
      line: {
        type: 'LineChart'
      },
      pie: {
        type: 'PieChart',
        multiChartIndexes: [0] // Ideally this would be chosen by the user (count, which)
      },
      heatgrid: {
        type: 'HeatGridChart'
      },
      stackedBar100: {
        type: 'NormalizedBarChart'
      },
      area: {
        type: 'StackedAreaChart'
      },
      dot: {
        type: 'DotChart'
      },
      waterfall: {
        type: 'WaterfallChart'
      },
      treemap: {
        type: 'TreemapChart'
      },
      sunburst: {
        type: 'SunburstChart'
      },
      multiplesunburst: {
        type: 'SunburstChart',
        multiChartIndexes: [1],
        dataMeasuresInColumns: true,
        orientation: 'vertical',
        smallTitlePosition: 'top',
        multiChartMax: 30,
        multiChartColumnsMax: Math.floor(this.cccOptions.width / 200),
        smallWidth: 200,
        smallHeight: 150,
        seriesInRows: false
      }
    };

    let keyOpts;

    if (key === null || key === '') {
    } else if (key === 'sunburst') {
      $(this.el)
        .find('.sku-zoom-button-group a')
        .hide();
      this.type = key;

      keyOpts = keyOptions[key];

      this.sunburst(keyOpts);

      if (this.hasProcessed) {
        this.render();
      }
    } else if (keyOptions.hasOwnProperty(key)) {
      $(this.el)
        .find('.sku-zoom-button-group a')
        .hide();
      this.type = key;

      keyOpts = keyOptions[key];

      this.cccOptions = this.getQuickOptions(keyOpts);

      if (this.chartDefinition !== null && this.chartDefinition !== undefined) {
        this.chartDefinition.type = keyOpts.type;
      }

      this.define_chart();

      if (this.hasProcessed) {
        this.render();
      }
    } else {
      if (key !== 'charteditor') {
        alert(`Do not support chart type: '${key}'`);
      }
    }
  }

  sunburst(keyOpts) {
    const data = this.processDataTree({ data: this.rawdata });
    const options = this.getQuickOptions(keyOpts);

    this.type = 'sunburst';

    const nodes = pv.dom(data).nodes();

    const tipOptions = {
      delayIn: 200,
      delayOut: 80,
      offset: 2,
      html: true,
      gravity: 'nw',
      fade: false,
      followMouse: true,
      corners: true,
      arrow: false,
      opacity: 1
    };

    const vis = new pv.Panel()
      .width(options.width)
      .height(options.height)
      .canvas(options.canvas);

    const partition = vis
      .add(pv.Layout.Partition.Fill)
      .nodes(nodes)
      .size(({ nodeValue }) => nodeValue)
      .order('descending')
      .orient('radial');

    partition.node
      .add(pv.Wedge)
      .fillStyle(
        pv
          .colors(options.colors)
          .by(({ parentNode }) => parentNode && parentNode.nodeName)
      )
      .visible(({ depth }) => depth > 0)
      .strokeStyle('#000')
      .lineWidth(0.5)
      .text(({ nodeValue, nodeName }) => {
        let v = '';

        if (typeof nodeValue !== 'undefined') {
          v = ` : ${nodeValue}`;
        }

        return nodeName + v;
      })
      .cursor('pointer')
      .events('all')
      .event('mousemove', pv.Behavior.tipsy(tipOptions));

    partition.label
      .add(pv.Label)
      .visible(({ angle, outerRadius }) => angle * outerRadius >= 6);

    this.chart = vis;
  }

  getQuickOptions(baseOptions) {
    const chartType = (baseOptions && baseOptions.type) || 'BarChart';
    const options = _.extend(
      {
        type: chartType,
        canvas: `canvas_${this.id}`
      },
      CCC_OPTIONS_DEFAULT.Base,
      CCC_OPTIONS_DEFAULT[chartType], // May be undefined
      baseOptions
    );

    if (this.adjustSizeTo) {
      const al = $(this.adjustSizeTo);

      if (al && al.length > 0) {
        const runtimeWidth = al.width() - 40;
        const runtimeHeight = al.height() - 40;

        if (runtimeWidth > 0) {
          options.width = runtimeWidth;
        }

        if (runtimeHeight > 0) {
          options.height = runtimeHeight;
        }
      }
    }

    if (this.data !== null && this.data.resultset.length > 5) {
      if (options.type === 'HeatGridChart') {
        // options.xAxisSize = 200;
      } else if (options.orientation !== 'horizontal') {
        options.extensionPoints = _.extend(
          Object.create(options.extensionPoints || {}),
          {
            xAxisLabel_textAngle: -Math.PI / 2,
            xAxisLabel_textAlign: 'right',
            xAxisLabel_textBaseline: 'middle'
          }
        );
      }
    }

    options.colors = Settings.CHART_COLORS;

    return options;
  }

  define_chart(displayOptions) {
    if (!this.hasProcessed) {
      this.processDataTree({ data: this.rawdata }, true, true);
    }

    const self = this;
    const workspaceResults = this.adjustSizeTo
      ? $(this.adjustSizeTo)
      : $(this.el);
    const isSmall =
      this.data !== null && this.data.height < 80 && this.data.width < 80;
    const isMedium =
      this.data !== null && this.data.height < 300 && this.data.width < 300;
    const isBig = !isSmall && !isMedium;
    const animate = false;
    const hoverable = isSmall;

    let runtimeWidth = workspaceResults.width() - 40;
    let runtimeHeight = workspaceResults.height() - 40;

    let runtimeChartDefinition = _.clone(this.cccOptions);

    if (displayOptions && displayOptions.width) {
      runtimeWidth = displayOptions.width;
    }

    if (displayOptions && displayOptions.height) {
      runtimeHeight = displayOptions.height;
    }

    if (runtimeWidth > 0) {
      runtimeChartDefinition.width = runtimeWidth;
    }

    if (runtimeHeight > 0) {
      runtimeChartDefinition.height = runtimeHeight;
    }

    if (isBig) {
      if (
        runtimeChartDefinition.hasOwnProperty('extensionPoints') &&
        runtimeChartDefinition.extensionPoints.hasOwnProperty(
          'line_interpolate'
        )
      ) {
        delete runtimeChartDefinition.extensionPoints.line_interpolate;
      }
      if (
        runtimeChartDefinition.hasOwnProperty('extensionPoints') &&
        runtimeChartDefinition.extensionPoints.hasOwnProperty(
          'area_interpolate'
        )
      ) {
        delete runtimeChartDefinition.extensionPoints.area_interpolate;
      }
    }

    const zoomDefinition = {
      legend: {
        scenes: {
          item: {
            execute() {
              const chart = this.chart();

              if (!chart.hasOwnProperty('keptVisibleDatumSet')) {
                chart.keptVisibleDatumSet = [];
              }

              const keptSet =
                chart.keptVisibleDatumSet.length > 0
                  ? chart.keptVisibleDatumSet[
                      chart.keptVisibleDatumSet.length - 1
                    ]
                  : [];
              const zoomedIn = keptSet.length > 0;

              if (zoomedIn) {
                _.intersection(this.datums().array(), keptSet).forEach(
                  datum => {
                    datum.toggleVisible();
                  }
                );
              } else {
                pvc.data.Data.toggleVisible(this.datums());
              }

              this.chart().render(true, true, false);
            }
          }
        }
      },
      userSelectionAction(selectingDatums) {
        if (selectingDatums.length === 0) {
          return [];
        }

        const chart = self.chart.root;
        const data = chart.data;
        const selfChart = this.chart;

        if (!selfChart.hasOwnProperty('keptVisibleDatumSet')) {
          selfChart.keptVisibleDatumSet = [];
        }

        // We have too many datums to process setVisible = false initially
        if (data.datums().count() > 1500) {
          pvc.data.Data.setSelected(selectingDatums, true);
        } else if (
          data.datums(null, { visible: true }).count() === data.datums().count()
        ) {
          $(self.el)
            .find('.sku-button-zoomout, .sku-button-rerender')
            .show();

          const all = data.datums().array();

          _.each(_.difference(all, selectingDatums), datum => {
            datum.setVisible(false);
          });

          selfChart.keptVisibleDatumSet = [];
          selfChart.keptVisibleDatumSet.push(selectingDatums);
        } else {
          const kept =
            selfChart.keptVisibleDatumSet.length > 0
              ? selfChart.keptVisibleDatumSet[
                  selfChart.keptVisibleDatumSet.length - 1
                ]
              : [];

          const visibleOnes = data.datums(null, { visible: true }).array();

          if (visibleOnes.length < kept.length) {
            selfChart.keptVisibleDatumSet.push(visibleOnes);
          }

          const newSelection = [];

          _.each(_.difference(visibleOnes, selectingDatums), datum => {
            datum.setVisible(false);
          });

          _.each(_.intersection(visibleOnes, selectingDatums), datum => {
            newSelection.push(datum);
          });

          if (newSelection.length > 0) {
            selfChart.keptVisibleDatumSet.push(newSelection);
          }
        }

        chart.render(true, true, false);

        return [];
      }
    };

    runtimeChartDefinition = _.extend(
      runtimeChartDefinition,
      {
        hoverable,
        animate
      },
      this.chartDefinition
    );
    // if (this.chartDefinition !== null && this.chartDefinition !== undefined && this.chartDefinition.legend === true) {
    if (this.zoom) {
      const legend = runtimeChartDefinition.legend;

      runtimeChartDefinition = _.extend(runtimeChartDefinition, zoomDefinition);

      if (legend === false) {
        runtimeChartDefinition.legend = false;
      }
    }
    // }

    if (
      runtimeChartDefinition.type === 'TreemapChart' &&
      runtimeChartDefinition.legend
    ) {
      runtimeChartDefinition.legend.scenes.item.labelText = function() {
        let indent = '';
        const group = this.group;
        if (group) {
          const depth = group.depth;
          // 0 ""
          // 1 "text"
          // 2 "└ text"
          // 3 "  └ text"
          switch (depth) {
            case 0:
              return '';
            case 1:
              break;
            case 2:
              indent = ' └ ';
              break;
            default:
              indent = `${new Array(2 * (depth - 2) + 1).join(' ')} └ `;
          }
        }
        return indent + this.base();
      };
    }

    this.chart = new pvc[runtimeChartDefinition.type](runtimeChartDefinition);
    this.chart.setData(this.data, {
      crosstabMode: true,
      seriesInRows: false
    });
  }

  renderChartElement(context) {
    const self = context || this;
    const isSmall =
      self.data !== null && self.data.height < 80 && self.data.width < 80;
    const isMedium =
      self.data !== null && self.data.height < 300 && self.data.width < 300;
    const isBig = !isSmall && !isMedium;
    let animate = false;

    if (self.chart.options && self.chart.options.animate) {
      animate = true;
    }

    if (
      !animate ||
      $(self.el)
        .find('.sku-canvas-wrapper')
        .is(':visible')
    ) {
      $(self.el)
        .find('.sku-canvas-wrapper')
        .hide();
    }

    try {
      if (animate) {
        $(self.el)
          .find('.sku-canvas-wrapper')
          .show();
      }

      self.chart.render();
      self.hasRendered = true;
    } catch (e) {
      $(`#canvas_${self.id}`).text(`Could not render chart${e}`);
    }

    if (self.chart.options && self.chart.options.animate) {
      return false;
    }

    if (isBig) {
      $(self.el)
        .find('.sku-canvas-wrapper')
        .show();
    } else {
      $(self.el)
        .find('.sku-canvas-wrapper')
        .fadeIn(400);
    }

    return false;
  }

  processDataTree(args, flat, setdata) {
    const data = {};

    if (flat) {
      data.resultset = [];
      data.metadata = [];
      data.height = 0;
      data.width = 0;
    }

    let currentDataPos = data;

    if (typeof args === 'undefined' || typeof args.data === 'undefined') {
      return;
    }

    if (args.data !== null && args.data.error !== null) {
      return;
    }

    // Check to see if there is data
    if (
      args.data === null ||
      (args.data.cellset && args.data.cellset.length === 0)
    ) {
      return;
    }

    const cellset = args.data.cellset;

    if (cellset && cellset.length > 0) {
      const reduceFunction = (memo, num) => memo + num;
      let lowest_level = 0;
      let data_start = 0;
      let hasStart = false;
      let row;
      let rowLen;
      let labelCol;

      for (
        row = 0, rowLen = cellset.length;
        data_start === 0 && row < rowLen;
        row++
      ) {
        for (
          let field = 0, fieldLen = cellset[row].length;
          field < fieldLen;
          field++
        ) {
          if (!hasStart) {
            while (
              cellset[row][field].type === COLUMN_HEADER &&
              cellset[row][field].value === 'null'
            ) {
              row++;
            }
          }

          hasStart = true;

          if (cellset[row][field].type === ROW_HEADER_HEADER) {
            while (cellset[row][field].type === ROW_HEADER_HEADER) {
              if (flat) {
                data.metadata.push({
                  colIndex: field,
                  colType: 'String',
                  colName: cellset[row][field].value
                });
              }

              field++;
            }

            lowest_level = field - 1;
          }

          if (cellset[row][field].type === COLUMN_HEADER) {
            let lowest_col_header = 0;
            const colheader = [];

            while (lowest_col_header <= row) {
              if (cellset[lowest_col_header][field].value !== 'null') {
                colheader.push(cellset[lowest_col_header][field].value);
              }

              lowest_col_header++;
            }

            if (flat) {
              data.metadata.push({
                colIndex: field,
                colType: 'Numeric',
                colName: colheader.join(' ~ ')
              });
            }

            data_start = row + 1;
          }
        }
      }

      const rowlabels = [];

      for (labelCol = 0; labelCol <= lowest_level; labelCol++) {
        rowlabels.push(null);
      }

      for (row = data_start, rowLen = cellset.length; row < rowLen; row++) {
        if (cellset[row][0].value !== '') {
          const record = [];
          let flatrecord = [];
          let parent = null;
          let rv = null;

          for (labelCol = 0; labelCol <= lowest_level; labelCol++) {
            if (cellset[row] && cellset[row][labelCol].value === 'null') {
              let prevLabel = 0;
              currentDataPos = data;

              for (
                ;
                prevLabel < lowest_level &&
                cellset[row][prevLabel].value === 'null';
                prevLabel++
              ) {
                currentDataPos = currentDataPos[rowlabels[prevLabel]];
              }

              if (prevLabel > labelCol) {
                labelCol = prevLabel;
              }
            }

            if (cellset[row] && cellset[row][labelCol].value !== 'null') {
              if (labelCol === 0) {
                for (let xx = 0; xx <= lowest_level; xx++) {
                  rowlabels[xx] = null;
                }
              }

              if (typeof currentDataPos === 'number') {
                parent[rv] = {};
                currentDataPos = parent[rv];
              }

              rv = cellset[row][labelCol].value;
              rowlabels[labelCol] = rv;

              if (!currentDataPos.hasOwnProperty(rv)) {
                currentDataPos[rv] = {};
              }

              parent = currentDataPos;
              currentDataPos = currentDataPos[rv];
            }
          }

          flatrecord = _.clone(rowlabels);

          for (
            let col = lowest_level + 1, colLen = cellset[row].length;
            col < colLen;
            col++
          ) {
            const cell = cellset[row][col];
            let value = cell.value || 0;
            let maybePercentage = value !== 0;
            const raw = cell.properties.raw;

            // Check if the resultset contains the raw value,
            // if not try to parse the given value
            if (raw && raw !== 'null') {
              value = parseFloat(raw);
            } else if (
              typeof cell.value !== 'number' &&
              parseFloat(cell.value.replace(/[^a-zA-Z 0-9.]+/g, ''))
            ) {
              value = parseFloat(cell.value.replace(/[^a-zA-Z 0-9.]+/g, ''));
              maybePercentage = false;
            }

            if (value > 0 && maybePercentage) {
              value =
                cell.value && cell.value.includes('%') ? value * 100 : value;
            }

            record.push(value);
            flatrecord.push({ f: cell.value, v: value });
          }

          if (flat) {
            data.resultset.push(flatrecord);
          }

          const sum = _.reduce(record, reduceFunction, 0);

          rv = rv === null ? 'null' : rv;
          parent[rv] = sum;
          currentDataPos = data;
        }
      }

      if (setdata) {
        this.rawdata = args.data;
        this.data = data;
        this.hasProcessed = true;
        this.data.height = this.data.resultset.length;
      }

      return data;
    } else {
      $(this.el)
        .find('.sku-canvas-wrapper')
        .text('No results')
        .show();
    }
  }
}

export default SaikuChartRenderer;
